package com.chukun.redis.lock;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * @author chukun
 * @version 1.0.0
 * @description 基于redis实现分布式锁
 * @createTime 2023年03月18日 20:59:00
 */
@Slf4j
public class RedisDistributeLock implements Lock {

    /**
     * 请求加锁的超时时间不限
     */
    private static final long UN_LIMIT_TIME = -1L;

    /**
     * 定时器，后期优化
     */
    private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);


    private StringRedisTemplate stringRedisTemplate;

    /**
     * 锁名称
     */
    private String lockName;

    /**
     * 锁的value值
     */
    private String lockValue;

    /**
     * 锁的过期时间
     */
    private long expireTime;

    /**
     * 获取锁的时间
     */
    private long tryLockTime;

    /**
     * 获取锁的时间单位
     */
    private TimeUnit tryLockTimeUnit;

    /**
     * redis分布式锁加锁的lua脚本
     */
    private static final String REDIS_LOCK_SCRIPT =
            "if redis.call('exists',KEYS[1]) == 0 or redis.call('hexists',KEYS[1],ARGV[1]) == 1 then   " +
                    "redis.call('hincrby',KEYS[1],ARGV[1],1)   " +
                    "redis.call('expire',KEYS[1],ARGV[2])  " +
                    "return 1 " +
            "else  " +
                    "return 0  " +
            "end";

    /**
     * redis分布式锁解锁的lua脚本
     */
    private static final String REDIS_UN_LOCK_SCRIPT =
            "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 0 then    " +
                    "return -1  " +
            "elseif redis.call('HINCRBY',KEYS[1],ARGV[1],-1) == 0 then   " +
                    "return redis.call('del',KEYS[1])  " +
            "else   " +
                    "return 0 " +
            "end";

    /**
     * redis分布锁续租的lua脚本
     */
    private static final String RENEW_EXPIRE_SCRIPT =
            "if redis.call('HEXISTS',KEYS[1],ARGV[1]) == 1 then     " +
                    "return redis.call('expire',KEYS[1],ARGV[2]) " +
            "else     " +
                    "return 0 " +
            "end";


    /**
     * 构造器
     * @param stringRedisTemplate
     * @param lockName
     * @param lockValue
     */
    public RedisDistributeLock(StringRedisTemplate stringRedisTemplate, String lockName, String lockValue) {
        this.lockName = lockName;
        this.lockValue = lockValue;
        this.expireTime = 50L;
        this.tryLockTime = 100L;
        this.tryLockTimeUnit = TimeUnit.MILLISECONDS;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 构造器
     * @param stringRedisTemplate
     * @param lockName
     * @param lockValue
     * @param expireTime
     */
    public RedisDistributeLock(StringRedisTemplate stringRedisTemplate, String lockName, String lockValue, long expireTime) {
        this.lockName = lockName;
        this.lockValue = lockValue;
        this.expireTime = expireTime;
        this.tryLockTime = 100L;
        this.tryLockTimeUnit = TimeUnit.MILLISECONDS;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 构造器
     * @param stringRedisTemplate
     * @param lockName
     * @param lockValue
     * @param expireTime
     * @param tryLockTime
     */
    public RedisDistributeLock(StringRedisTemplate stringRedisTemplate, String lockName, String lockValue, long expireTime, long tryLockTime) {
        this.lockName = lockName;
        this.lockValue = lockValue;
        this.expireTime = expireTime;
        this.tryLockTime = tryLockTime;
        this.tryLockTimeUnit = TimeUnit.MILLISECONDS;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public void lock() {
        tryLock();
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {}

    @Override
    public boolean tryLock() {
        try {
            return tryLock(tryLockTime, tryLockTimeUnit);
        } catch (Exception e) {
            log.error("RedisDistributeLock.tryLock.error", e);
        }
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        if (time == UN_LIMIT_TIME) {
            Boolean execute = stringRedisTemplate.execute(new DefaultRedisScript<>(REDIS_LOCK_SCRIPT, Boolean.class), Collections.singletonList(lockName), lockValue, expireTime);
            while (Boolean.FALSE.equals(execute)) {
                execute = stringRedisTemplate.execute(new DefaultRedisScript<>(REDIS_LOCK_SCRIPT, Boolean.class), Collections.singletonList(lockName), lockValue, expireTime);
                // 休眠 5毫秒
                TimeUnit.MILLISECONDS.sleep(5);
            }
            // redis分布式锁续租
            this.renewExpire();
            return true;
        }
        // 带超时时间的重试加锁
        long expireTime = System.currentTimeMillis() + time;
        boolean isTimeout = false;
        Boolean execute = stringRedisTemplate.execute(new DefaultRedisScript<>(REDIS_LOCK_SCRIPT, Boolean.class), Collections.singletonList(lockName), lockValue, expireTime);
        while (Boolean.FALSE.equals(execute)) {
            execute = stringRedisTemplate.execute(new DefaultRedisScript<>(REDIS_LOCK_SCRIPT, Boolean.class), Collections.singletonList(lockName), lockValue, expireTime);
            // 休眠 5毫秒
            TimeUnit.MILLISECONDS.sleep(1);
            isTimeout = System.currentTimeMillis() > expireTime;
        }
        // 加锁成功
        boolean flag = !isTimeout && Boolean.TRUE.equals(execute);
        if (flag) {
            // redis分布式锁续租
            this.renewExpire();
        }
        return flag;
    }

    @Override
    public void unlock() {
        Long execute = stringRedisTemplate.execute(new DefaultRedisScript<>(REDIS_UN_LOCK_SCRIPT, Long.class), Collections.singletonList(lockName), lockValue);
        if (execute == null) {
            throw new RuntimeException("this lock doesn't exists.");
        }
    }

    /**
     * 续租程序
     */
    private void renewExpire() {
        scheduledThreadPoolExecutor.scheduleAtFixedRate(() -> {
            stringRedisTemplate.execute(new DefaultRedisScript<>(RENEW_EXPIRE_SCRIPT, Boolean.class), Collections.singletonList(lockName), lockValue, expireTime);
        }, 1, expireTime/3, TimeUnit.MICROSECONDS);

    }

    @Override
    public Condition newCondition() {
        return null;
    }
}
